Skip to content

fix: standalone dependency resolution#765

Merged
james-elicx merged 2 commits intocloudflare:mainfrom
hyoban:4-3-stand
Apr 3, 2026
Merged

fix: standalone dependency resolution#765
james-elicx merged 2 commits intocloudflare:mainfrom
hyoban:4-3-stand

Conversation

@hyoban
Copy link
Copy Markdown
Contributor

@hyoban hyoban commented Apr 3, 2026

  • skip node or virtual deps
  • resolve package.json for rsc-html-stream

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 3, 2026

Open in StackBlitz

npm i https://pkg.pr.new/vinext@765

commit: bd49051

lyzno1

This comment was marked as resolved.

@james-elicx
Copy link
Copy Markdown
Collaborator

/bigbonk review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good fix for two real issues in standalone dependency resolution. The changes are well-scoped and the tests cover the right scenarios.

Summary of what this does:

  1. server-externals-manifest.tspackageNameFromSpecifier now correctly filters out Node builtins (crypto, fs/promises), virtual modules (virtual:vite-rsc), and intra-bundle file references (index.js, assets/chunk-abc.js). Previously these could leak into the externals manifest, causing the standalone BFS to try (and fail) to resolve them as npm packages.

  2. standalone.ts — Adds a resolve.paths() fallback for packages like rsc-html-stream that only expose subpath exports (e.g. ./server but not . or ./package.json). resolver.resolve() can't access the package.json through the exports map, but resolve.paths() gives us the node_modules lookup directories to check directly.

Both fixes are correct and the tests are thorough. A couple of minor observations below, nothing blocking.


// External specifiers can include non-package schemes such as
// "virtual:vite-rsc" or "file:...". Those are never npm packages.
if (/^[a-zA-Z][a-zA-Z\d+.-]*:/.test(specifier)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: this regex matches RFC 3986 scheme syntax, which is the right call. Worth noting it also matches Windows drive letters like C:/path, but that's harmless since (a) those would never appear as Rollup external specifiers, and (b) the \\ startsWith check above catches the backslash variant.

One thing I noticed: the node: prefix check that was in the original code (specifier.startsWith("node:")) is now redundant since the regex catches it. But removing it was the right move — the regex is more general and handles virtual:, file:, data:, etc. all at once. Clean.


return specifier.split("/")[0] || null;
const packageName = specifier.split("/")[0] || null;
if (!packageName || BUILTIN_MODULES.has(specifier) || BUILTIN_MODULES.has(packageName)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking both specifier and packageName against builtins is a nice touch — handles fs/promises (where the specifier itself is a builtin subpath) and bare builtins like crypto (where packageName equals the specifier). The builtinModules list includes entries like fs/promises directly, so both paths are necessary.

return null;
}

if (specifier.startsWith("@")) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: scoped packages (@scope/pkg) aren't checked against BUILTIN_MODULES before returning. This is fine because no Node builtin starts with @, but it does mean if Node ever added a @node/something builtin (unlikely), it would slip through. Not worth changing, just noting the asymmetry for future readers.

// which exports `./server` but not `.` or `./package.json`). resolver.resolve()
// cannot access those hidden paths, but Node still exposes the installed
// node_modules lookup locations via resolve.paths().
const lookupPaths = resolver.resolve.paths(packageName) ?? [];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good fallback strategy. resolve.paths() is stable Node API and gives the same lookup directories that require.resolve() would search. The pkg.name === packageName guard on line 87 prevents false positives from similarly-named directories that aren't the right package (e.g., a stale directory left over from a different version).


try {
const plugin = createServerExternalsManifestPlugin();
const writeBundle = (plugin.writeBundle as { handler: Function }).handler;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the as { handler: Function } cast is a reasonable workaround for Vite's plugin types (where writeBundle can be either a function or an object with handler). If this pattern shows up in more tests, it might be worth a small getPluginHook test helper to reduce the casting boilerplate, but fine as-is for a single test.

@@ -0,0 +1,53 @@
import { describe, expect, it } from "vite-plus/test";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a few direct unit tests for packageNameFromSpecifier — it's now exported and the logic is non-trivial (regex, builtin set, scoped packages). The integration test here is valuable but a table-driven unit test would catch regressions faster and document the edge cases explicitly. Not blocking — the integration test covers the important paths.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk bot commented Apr 3, 2026

Review submitted. Here's a summary of my findings:

Approved — this is a clean, well-tested fix for two real issues:

  1. packageNameFromSpecifier improvements — The function now correctly filters out Node builtins (crypto, fs/promises), virtual/scheme-prefixed modules (virtual:vite-rsc), and intra-bundle file references. The approach is sound: a URI scheme regex catches all foo: patterns (including node:, virtual:, file:, etc.), and a BUILTIN_MODULES set catches bare builtins like crypto and subpath builtins like fs/promises. The bundleFiles set pre-check avoids false positives from chunk filenames that look like package names.

  2. resolve.paths() fallback in standalone.ts — Handles packages like rsc-html-stream that only expose subpath exports and don't expose ./package.json. The fallback uses Node's resolve.paths() to get node_modules lookup directories and checks each one directly, with a pkg.name guard to prevent false matches.

Minor suggestions in the review: consider adding direct unit tests for the now-exported packageNameFromSpecifier function (table-driven tests documenting edge cases), but this is not blocking.

github run

@james-elicx james-elicx merged commit bb6dda8 into cloudflare:main Apr 3, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants